package remote;

import infrastructure.exceptions.BaseServerRuntimeException;
import infrastructure.exceptions.NetworkException;
import infrastructure.exceptions.ServerFataError;

import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.Socket;

/**
 * This class represents client side connection to the remote host
 * @author sashas
 *
 */
public class Connection {
	
	private class WrappedSocketConnection{
		
		/** Keeps actual socket*/
		private Socket sock;
		
		WrappedSocketConnection(String hostName,int port) throws IOException{
			sock = new Socket(hostName,port);
			sock.setSendBufferSize(440000);
		}
		
		Socket getSocket() throws IOException{
			
			if (!sock.isConnected()){				
				sock.close();
				sock = new Socket(sock.getInetAddress(),sock.getPort());
				sock.setSendBufferSize(440000);
			}
			
			return sock;
		}
		
		
		void close() throws IOException{
			sock.close();
		}
	}
	
	
	/** keeps socket of the connection*/
	private WrappedSocketConnection actualConnection;
	
	private Connection(String hostname,int port) throws NetworkException{		
		
		try{
			actualConnection = new WrappedSocketConnection(hostname,port);
		}catch(IOException ex){
			throw new NetworkException(ex);
		}
		
		
	}
	
	
	public static Connection getConnection(String hostname,int port) throws NetworkException{
		return new Connection(hostname, port);		
	}
	
	public void close(){
		try{
			actualConnection.close();
		}catch(IOException ex){
			ex.printStackTrace();
		}
		
	}
	
	/**
	 * @param <T> - return type of the method 
	 * @param invocation - invocation procedure 
	 * @return procedue result
	 */
	public <T> T invoke(RemoteInvocation invocation) throws BaseServerRuntimeException{
		
		Object result = null;
		try{			
			
			ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
			ObjectOutputStream out = new ObjectOutputStream(bytesOut);
			// write request 
			//ObjectOutputStream out = new ObjectOutputStream(new BufferedOutputStream(actualConnection.getSocket().getOutputStream()));
			
			out.writeObject(invocation);			
			DataOutputStream dataOut = new DataOutputStream(new BufferedOutputStream(actualConnection.getSocket().getOutputStream()));
			dataOut.writeInt(bytesOut.size());
			bytesOut.writeTo(dataOut);
			dataOut.flush();
			
			// read responce
			ObjectInputStream in = new ObjectInputStream(actualConnection.getSocket().getInputStream());
			result = in.readObject();
		}catch(Exception ex){
			throw new ServerFataError("Unable to invoke", ex);
		}
		
		if (result instanceof BaseServerRuntimeException){
			throw (BaseServerRuntimeException)result;		
		}else if (result instanceof Throwable){
			throw new ServerFataError("Unexpected exception", (Throwable)result);
		}else{				
			try{	
				T resultObject = (T)result;
				return resultObject;
			}catch(ClassCastException ex){
				throw new ServerFataError("Unexpected result", ex);
			}
		}					
		
	}
	
	
}
